package com.swak.fx.support.ability;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Set;

import com.sun.javafx.geom.RectBounds;
import com.sun.javafx.scene.NodeHelper;
import com.sun.javafx.scene.text.GlyphList;
import com.sun.javafx.scene.text.TextLayout;
import com.sun.javafx.scene.text.TextLine;
import com.swak.fx.support.Display;
import com.swak.utils.Fmt;
import com.swak.utils.Lists;
import com.swak.utils.Sets;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Bounds;
import javafx.scene.CacheHint;
import javafx.scene.Node;
import javafx.scene.effect.BlendMode;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.paint.Paint;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Text;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;

/**
 * SelectionAbility.
 * 
 * @author DELL
 */
public class SelectionAbility {

	private static Method textLayoutMethod;
	{
		try {
			textLayoutMethod = Text.class.getDeclaredMethod("getTextLayout");
			textLayoutMethod.setAccessible(true);
		} catch (Exception e) {
		}
	}

	private double selectionMinX;
	private double selectionMinY;

	private final Selection mouseSelection = new Selection();
	private final List<Selection> selections = Lists.newArrayList();
	private final ArrayList<Rectangle> rectangles = new ArrayList<>();
	private final ObjectProperty<Paint> paint = new SimpleObjectProperty<>(Color.rgb(255, 0, 0, 0.4));
	private final Pane parent;
	private final Comparator<Bound> comparator = (o1, o2) -> {
		if (o1.getMinY() < o2.getMinY()) {
			return -1;
		} else if (o1.getMinY() == o2.getMinY() && o1.getMinX() < o2.getMinX()) {
			return -1;
		}
		return 1;
	};

	public SelectionAbility(Pane parent) {
		this.parent = parent;
		this.init();
	}

	private void init() {
		this.parent.addEventHandler(MouseEvent.MOUSE_PRESSED, e -> this.startSelection(e));
		this.parent.addEventHandler(MouseEvent.MOUSE_DRAGGED, e -> this.draggedSelection(e));
		this.parent.addEventHandler(MouseEvent.MOUSE_RELEASED, e -> this.endSelection(e));
	}

	public ObjectProperty<Paint> paintProperty() {
		return paint;
	}

	public String getSelectionText() {
		StringBuilder texts = new StringBuilder();
		for (Selection selection : selections) {
			String text = selection.getText().getText();
			try {
				int start = Math.max(selection.getStart(), 0);
				int end = Math.min(selection.getEnd() + 1, text.length());
				texts.append(text.substring(start, end));
			} catch (Exception e) {
//				System.out.println(
//						StringUtils.format("(%s,%s) - %s", selection.getStart(), selection.getEnd() + 1, text));
			}
		}
		return texts.toString();
	}

	private final TextLayout getTextLayout(Text text) {
		TextLayout textLayout = null;
		try {
			textLayout = (TextLayout) textLayoutMethod.invoke(text);
		} catch (IllegalAccessException e) {
		} catch (InvocationTargetException e) {
		}
		return textLayout;
	}

	private final Set<Node> getTextNodes(Node pane) {
		Set<Node> labeledTextNodes = pane.lookupAll("LabeledText");
		Set<Node> textNodes = pane.lookupAll("Text");
		Set<Node> nodes = Sets.newOrderSet();
		if (pane instanceof Text) {
			nodes.add(pane);
		}
		nodes.addAll(labeledTextNodes);
		nodes.addAll(textNodes);
		return nodes;
	}

	private void startSelection(MouseEvent e) {
		if (e.getButton() != MouseButton.PRIMARY) {
			return;
		}
		this.selectionMinX = e.getSceneX();
		this.selectionMinY = e.getSceneY();
		Display.runUI(() -> {
			this.parent.getChildren().removeAll(rectangles);
			this.rectangles.clear();
			this.selections.clear();
		});
	}

	private void draggedSelection(MouseEvent e) {
		double endX = e.getSceneX();
		double endY = e.getSceneY();
		double _startX = Math.min(this.selectionMinX, endX);
		double _endX = Math.max(this.selectionMinX, endX);
		double _startY = Math.min(this.selectionMinY, endY);
		double _endY = Math.max(this.selectionMinY, endY);
		this.mouseSelection.setMinX(_startX).setMinY(_startY).setMaxX(_endX).setMaxY(_endY);

		handleTextLineBounds();
		this.selections.clear();

		for (TextLineBounds hold : textBoundsHolds) {
			Text textNode = hold.getText();
			TextLine line = hold.getLine();
			GlyphList chars = hold.getChars();
			Selection selection = hold.select();
			if (selection != null) {
				double startPosx = selection.getMinX() - hold.getOffsetX();
				double endPosx = selection.getMaxX() - hold.getOffsetX();
				int selectStart = -1;
				int selectEnd = -1;
				int glyphCount = chars.getGlyphCount();
				for (int index = 0; index < glyphCount; index++) {
					double posx = chars.getPosX(index);
					double nextPosx = index == glyphCount - 1 ? endPosx : chars.getPosX(index + 1);
					if (nextPosx > startPosx && posx <= endPosx) {
						if (selectStart == -1) {
							selectStart = index;
							selection.setMinX(posx + hold.getOffsetX());
						}
						selectEnd = index;
						selection.setMaxX(nextPosx + hold.getOffsetX());
						continue;
					}
					if (nextPosx <= posx) {
						break;
					}
				}
				selection.setText(textNode).setStart(line.getStart() + selectStart).setEnd(line.getStart() + selectEnd);
				selections.add(selection);
			}
		}

		Bounds contentBounds = parent.localToScene(parent.getBoundsInLocal());
		double offsetX = contentBounds.getMinX();
		double offsetY = contentBounds.getMinY();
		ArrayList<Rectangle> newRectangles = new ArrayList<>();
		for (Selection selection : selections) {
			Rectangle rect = new Rectangle();
			rect.setCacheHint(CacheHint.SPEED);
			rect.setCache(true);
			rect.setMouseTransparent(true);
			rect.setBlendMode(BlendMode.MULTIPLY);
			rect.fillProperty().bind(paintProperty());
			rect.setManaged(false);
			rect.setX(selection.getMinX() - offsetX);
			rect.setY(selection.getMinY() - offsetY);
			rect.setWidth(selection.getMaxX() - selection.getMinX());
			rect.setHeight(selection.getMaxY() - selection.getMinY());
			newRectangles.add(rect);
		}
		Display.runUI(() -> {
			this.parent.getChildren().removeAll(rectangles);
			this.rectangles.clear();
			this.rectangles.addAll(newRectangles);
			this.parent.getChildren().addAll(newRectangles);
		});
	}

	private List<TextLineBounds> textBoundsHolds;

	private void handleTextLineBounds() {
		if (textBoundsHolds != null) {
			return;
		}
		textBoundsHolds = Lists.newArrayList();
		Set<Node> texts = getTextNodes(this.parent);
		for (Node tnode : texts) {
			if (!NodeHelper.isTreeVisible(tnode)) {
				continue;
			}

			Fmt.out.println("*******************************");
			Text textNode = ((Text) tnode);
			Bounds textBounds = textNode.localToScene(textNode.getBoundsInLocal());
			Fmt.out.println(textNode.getText());
			Fmt.out.println(textBounds);

			TextLayout textLayout = getTextLayout(textNode);
			TextLine[] lines = textLayout.getLines();
			for (int i = 0; i < lines.length; i++) {
				TextLine line = lines[i];
				GlyphList textRun = line.getRuns()[0];
				RectBounds lineBounds = line.getBounds();
				Fmt.out.println(textRun + ":" + lineBounds);

				double lineWidth = lineBounds.getWidth();
				double lineHeight = lineBounds.getHeight();
				double lineMinX = textBounds.getMinX() + lineBounds.getMinX();
				double lineMaxX = Math.min(textBounds.getMinX() + lineBounds.getMinX() + lineWidth,
						textBounds.getMaxX());
				double lineMinY = textBounds.getMinY() + lineHeight * i;
				double lineMaxY = textBounds.getMinY() + lineHeight * (i + 1);

				if (lineMaxY > textBounds.getMaxY()) {
					continue;
				}

				TextLineBounds selection = new TextLineBounds();
				selection.setMinX(Math.round(lineMinX)).setMaxX(Math.round(lineMaxX)).setMinY(Math.round(lineMinY))
						.setMaxY(Math.round(lineMaxY)).setText(textNode).setLine(line).setChars(textRun)
						.setOffsetX(textBounds.getMinX()).setOffsetY(textBounds.getMinY());
				textBoundsHolds.add(selection);
				
				Fmt.out.println(selection);
			}
		}

		Lists.sort(textBoundsHolds, comparator);

		for (int i = 1; i < textBoundsHolds.size(); i++) {
			TextLineBounds preSelection = textBoundsHolds.get(i - 1);
			TextLineBounds selection = textBoundsHolds.get(i);
			if (preSelection.getMinY() != selection.getMinY()) {
				continue;
			}
			if (preSelection.getMaxX() > selection.getMinX()) {
				preSelection.setMaxX(selection.getMinX());
			}
		}
	}

	private void endSelection(MouseEvent e) {
		this.selectionMinX = 0;
		this.selectionMinY = 0;
		this.mouseSelection.setMinX(0).setMinY(0).setMaxX(0).setMaxY(0);
	}

	/**
	 * 边框
	 * 
	 * @author 618lf
	 */
	@Getter
	@Setter
	@Accessors(chain = true)
	@ToString(exclude = { "text", "line", "chars" })
	private class TextLineBounds implements Bound {
		private double minX;
		private double minY;
		private double maxX;
		private double maxY;
		private double offsetX;
		private double offsetY;
		private Text text;
		private TextLine line;
		private GlyphList chars;

		public Selection select() {
			double minx = Math.max(mouseSelection.getMinX(), minX);
			double miny = Math.max(mouseSelection.getMinY(), minY);
			double maxx = Math.min(mouseSelection.getMaxX(), maxX);
			double maxy = Math.min(mouseSelection.getMaxY(), maxY);
			boolean crossed = minx <= maxx && miny <= maxy;
			if (crossed) {
				Selection selection = new Selection();
				selection.setMinX(minx).setMinY(minY).setMaxX(maxx).setMaxY(maxy);
				if (mouseSelection.getMaxY() <= maxY) {
					selection.setMaxY(maxY);
				}
				if (mouseSelection.getMaxY() > maxY) {
					selection.setMaxX(maxX);
				}
				if (mouseSelection.getMinY() < minY) {
					selection.setMinX(minX);
				}
				return selection;
			}
			return null;
		}
	}

	@Getter
	@Setter
	@Accessors(chain = true)
	@ToString(exclude = { "text" })
	private class Selection implements Bound {
		private double minX;
		private double minY;
		private double maxX;
		private double maxY;
		private Text text;
		private int start;
		private int end;
	}

	private interface Bound {
		double getMinX();

		double getMinY();

		double getMaxX();

		double getMaxY();
	}
}
